/////////////////////////////////////////////////////////////////////
//
//	UZipDotNet
//	ZIP File processing
//
//	AddFilesAndFoldersForm.cs
//	Windows form allowing the user to select files and folders
//	to be included in the ZIP archive.
//
//	Granotech Limited
//	Author: Uzi Granot
//	Version 1.0
//	March 30, 2012
//	Copyright (C) 2012 Granotech Limited. All Rights Reserved
//
//	Version 1.2 October 23, 2012
//		AddFilesAndFoldersForm.cs
//			Method: OnAddButton
//			Fix problen related to creating zip file of a folder at the root of the drive
//
//	UZipDotNet application is a free software.
//	It is distributed under the Code Project Open License (CPOL).
//	The document UZipDotNetReadmeAndLicense.pdf contained within
//	the distribution specify the license agreement and other
//	conditions and notes. You must read this document and agree
//	with the conditions specified in order to use this software.
//
/////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.IO;
using System.Drawing;

namespace UZipDotNet
{
public partial class AddFilesAndFoldersForm : Form
    {
    ////////////////////////////////////////////////////////////////////
    //	Members
    ////////////////////////////////////////////////////////////////////

    public  List<FileHeader>	AddDir;
    public  String				RootDir;
    public  Int32				RootDirPtr;
    public	Int32				CompLevel;

    private FileSystemWatcher	DirWatcher;
    private Timer				FileListTimer;
    private Boolean				RefreshFileList;
    private Boolean				ResizeLock;

    ////////////////////////////////////////////////////////////////////
    //	Constructor
    ////////////////////////////////////////////////////////////////////

    public AddFilesAndFoldersForm()
        {
        InitializeComponent();
        }

    ////////////////////////////////////////////////////////////////////
    //	On Load
    ////////////////////////////////////////////////////////////////////

    private void OnLoad
            (
            object sender,
            EventArgs e
            )
        {
        // program initialization
        ProgramInitialization();

        // Directory tree initialization
        DirectoryTreeInit();

        // resize
        OnResize(this, null);
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	One time program initialization
    ////////////////////////////////////////////////////////////////////

    private void ProgramInitialization()
        {
        // create image list
        DirectoryTree.ImageList = new ImageList();
        DirectoryTree.ImageList.Images.Add(Properties.Resources.MyComputerBmp);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.MyComputerSelBmp);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.DiskDriveBmp);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.DiskDriveSelBmp);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.Folder);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.FolderSel);
        DirectoryTree.ImageList.Images.Add(Properties.Resources.File);

        // attach the same list to the file tree
        FileList.SmallImageList = DirectoryTree.ImageList;

        // other options
        DirectoryTree.ShowLines = true;
        DirectoryTree.ShowRootLines = false;
        DirectoryTree.Scrollable = true;
        DirectoryTree.BorderStyle = BorderStyle.FixedSingle;
        DirectoryTree.ShowPlusMinus = true;

        // create a new FileSystemWatcher
        DirWatcher = new FileSystemWatcher();

        // watch for changes in LastAccess and LastWrite times, and renaming of files or directories
        DirWatcher.NotifyFilter = NotifyFilters.Attributes | NotifyFilters.LastWrite |
            NotifyFilters.Size | NotifyFilters.FileName | NotifyFilters.DirectoryName;

        // Add event handlers.
        DirWatcher.Changed += new FileSystemEventHandler(OnDirWatcherEvent);
        DirWatcher.Created += new FileSystemEventHandler(OnDirWatcherEvent);
        DirWatcher.Deleted += new FileSystemEventHandler(OnDirWatcherEvent);
        DirWatcher.Renamed += new RenamedEventHandler(OnDirWatcherEvent);

        // create file list timer
        FileListTimer = new Timer();
        FileListTimer.Tick += new EventHandler(OnFileListTimer);
        FileListTimer.Interval = 500;

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	Build initial tree. My computer plus drives
    ////////////////////////////////////////////////////////////////////

    private void DirectoryTreeInit()
        {
        // root node
        TreeNode RootNode = new TreeNode();
        RootNode.ImageIndex = (Int32) DispIcon.MyComputer;
        RootNode.SelectedImageIndex = (Int32) DispIcon.MyComputerSel;
        RootNode.Text = "My Computer";
        RootNode.Tag = String.Empty;
        DirectoryTree.Nodes.Add(RootNode);

        // drives child nodes
        DriveInfo[] Drives = DriveInfo.GetDrives();
        foreach(DriveInfo DI in Drives)
            {
            // ignore CD/DVD and drives not ready
            if(!DI.IsReady || DI.DriveType == DriveType.CDRom) continue;

            // build node
            TreeNode DriveNode = new TreeNode();
            DriveNode.ImageIndex = (Int32) DispIcon.DiskDrive;
            DriveNode.SelectedImageIndex = (Int32) DispIcon.DiskDriveSel;
            DriveNode.Text = String.Format("{0} [{1}]", String.IsNullOrEmpty(DI.VolumeLabel) ? "Drive" : DI.VolumeLabel, DI.Name);
            DriveNode.Tag = DI.RootDirectory.FullName;
            RootNode.Nodes.Add(DriveNode);

            // add one dummy child node
            DriveNode.Nodes.Add(new TreeNode());
            }

        // expend first level
        RootNode.Expand();

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	User double click a node in order to expand it
    ////////////////////////////////////////////////////////////////////

    private void OnBeforeExpand
            (
            Object sender,
            TreeViewCancelEventArgs e
            )
        {
        // do not expand if there are no subdirectories
        if(AddSubdirectories(e.Node)) e.Cancel = true;

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	Add one level of child nodes
    ////////////////////////////////////////////////////////////////////

    private Boolean AddSubdirectories
            (
            TreeNode	ParentNode
            )
        {
        // nothing to do if we have subdirectories
        if(ParentNode.Nodes[0].Tag != null) return(false);

        // remove the existing dummy record
        ParentNode.Nodes.RemoveAt(0);

        // directory info for current node
        DirectoryInfo DirInfo = new DirectoryInfo((String) ParentNode.Tag);

        // loop for all sub-directories
        foreach(DirectoryInfo Dir in DirInfo.GetDirectories())
            {
            // test for two special cases
            if(IsSpecialDirectory(Dir.Name)) continue;

            // build node
            TreeNode ChildNode = new TreeNode();
            ChildNode.ImageIndex = (Int32) DispIcon.Folder;
            ChildNode.SelectedImageIndex = (Int32) DispIcon.FolderSel;
            ChildNode.Text = Dir.Name;
            ChildNode.Tag = Dir.FullName;
            ParentNode.Nodes.Add(ChildNode);

            // add dummy grandchild record
            ChildNode.Nodes.Add(new TreeNode());
            }

        // exit
        return(ParentNode.Nodes.Count == 0);
        }

    ////////////////////////////////////////////////////////////////////
    //	Directory watcher detected a change
    ////////////////////////////////////////////////////////////////////

    void OnDirWatcherEvent
            (
            object sender,
            FileSystemEventArgs e
            )
        {
        // set the refresh request
        RefreshFileList = true;
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	Simulate after select directory event
    ////////////////////////////////////////////////////////////////////

    void OnFileListTimer
            (
            object sender,
            EventArgs e
            )
        {
        if(RefreshFileList) PopulateFileListView(DirectoryTree.SelectedNode);
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	On after select directory
    ////////////////////////////////////////////////////////////////////

    private void OnAfterSelectDirectory
            (
            object sender,
            TreeViewEventArgs e
            )
        {
        PopulateFileListView(e.Node);
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	Populate File List View
    ////////////////////////////////////////////////////////////////////

    private void PopulateFileListView
            (
            TreeNode ParentNode
            )
        {
        // stop the timer
        FileListTimer.Stop();

        // stop the directory watch program
        DirWatcher.EnableRaisingEvents = false;

        // reset the refresh flag
        RefreshFileList = false;

        // clear file list
        // this routine will call resize if the file list area has scroll bars
        FileList.Items.Clear();
        
        // root directory
        if(ParentNode.Level == 0)
            {
            // drives child nodes
            foreach(DriveInfo Drv in DriveInfo.GetDrives())
                {
                // ignore CD/DVD and drives not ready
                if(!Drv.IsReady || Drv.DriveType == DriveType.CDRom) continue;

                // add disk drive to file list
                ListViewItem Item = new ListViewItem(
                    String.Format("{0} [{1}]", String.IsNullOrEmpty(Drv.VolumeLabel) ? "Drive" : Drv.VolumeLabel, Drv.Name),
                    (Int32) DispIcon.DiskDrive);
                Item.Tag = Drv;
                FileList.Items.Add(Item);
                }
            }

        // not root
        else
            {
            // directory info for current node
            DirectoryInfo DirInfo = new DirectoryInfo((String) ParentNode.Tag);

            // loop for all subdirectories
            foreach(DirectoryInfo Dir in DirInfo.GetDirectories())
                {
                // test for special cases
                if(IsSpecialDirectory(Dir.Name)) continue;

                // directory item
                ListViewItem Item = new ListViewItem(Dir.Name, (Int32) DispIcon.Folder);
                Item.Tag = Dir;
                Item.SubItems.Add(FormatFileDateTime(Dir.LastAccessTime));
                Item.SubItems.Add(FormatFileAttributes(Dir.Attributes));
                FileList.Items.Add(Item);
                }

            // loop for all files
            foreach(FileInfo File in DirInfo.GetFiles())
                {
                // file item
                ListViewItem Item = new ListViewItem(File.Name, (Int32) DispIcon.File);
                Item.Tag = File;
                Item.SubItems.Add(FormatFileDateTime(File.LastAccessTime));
                Item.SubItems.Add(FormatFileAttributes(File.Attributes));
                Item.SubItems.Add(FormatFileSize(File.Length));
                FileList.Items.Add(Item);
                }

            // start the directory watch program
            DirWatcher.Path = (String) ParentNode.Tag;
            DirWatcher.EnableRaisingEvents = true;

            // start the timer
            FileListTimer.Start();
            }

        // adjust columns
        OnFileListResize(null, null);
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	User double click a directory within file list
    ////////////////////////////////////////////////////////////////////

    private void OnFileDoubleClick
            (
            object sender,
            EventArgs e
            )
        {
        // get selected list
        ListView.SelectedListViewItemCollection SelectedList = FileList.SelectedItems;

        // for double click only one is acceptable
        if(SelectedList.Count != 1) return;

        // get selected item
        ListViewItem Item = SelectedList[0];

        // path
        String Path;

        // double click on a directory
        if(Item.Tag.GetType() == typeof(DirectoryInfo))
            {
            Path = ((DirectoryInfo) Item.Tag).FullName;
            }

        // double click on a drive
        else if(Item.Tag.GetType() == typeof(DriveInfo))
            {
            Path = ((DriveInfo) Item.Tag).Name;
            }

        // double click on a file
        else
            {
            return;
            }

        // current selected node in view tree
        if(!DirectoryTree.SelectedNode.IsExpanded) DirectoryTree.SelectedNode.Expand();

        // search
        for(TreeNode ChildNode = DirectoryTree.SelectedNode.FirstNode; ChildNode != null; ChildNode = ChildNode.NextNode)
            {
            if((String) ChildNode.Tag == Path)
                {
                // NOTE: changing the selected node will activate OnAfterSelectDirectory()
                DirectoryTree.SelectedNode = ChildNode;
                break;
                }
            }

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	Select all Ctrl-A
    ////////////////////////////////////////////////////////////////////

    private void OnKeyDown(object sender, KeyEventArgs e)
        {
        if(FileList.Items.Count != 0 && e.Control && !e.Alt && !e.Shift && e.KeyCode == Keys.A)
            {
            foreach(ListViewItem LVI in FileList.Items) LVI.Selected = true;
            }
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	On add button
    ////////////////////////////////////////////////////////////////////

    private void OnAddButton
            (
            object sender,
            EventArgs e
            )
        {
        // get selected list
        ListView.SelectedListViewItemCollection SelectedList = FileList.SelectedItems;

        // the list is empty
        if(SelectedList.Count == 0) return;

        // trap duplicate file errors
        try
            {
            // zip file root directory
            RootDir = (String) DirectoryTree.SelectedNode.Tag;
            // Version 1.2 fix
            RootDirPtr = RootDir.Length;
            if(RootDirPtr != 3 || !Char.IsLetter(RootDir[0]) || RootDir[1] != ':' || RootDir[2] != '\\') RootDirPtr++;

            // create empty file list
            AddDir = new List<FileHeader>();

            // get all selected directories
            foreach(ListViewItem Item in SelectedList)
                {
                if(Item.Tag.GetType() == typeof(DirectoryInfo)) ProcessDirectory((DirectoryInfo) Item.Tag);
                }

            // get all selected files in the zip file root directory
            foreach(ListViewItem Item in SelectedList)
                {
                if(Item.Tag.GetType() == typeof(FileInfo))
                    {
                    // shortcut for file information
                    FileInfo FI = (FileInfo) Item.Tag;

                    // create zip directory file header record with name date time and attributes
                    FileHeader FH = new FileHeader(FI.FullName.Substring(RootDirPtr), FI.LastWriteTime, FI.Attributes, 0, FI.Length);

                    // find item poition in the list
                    Int32 Index = AddDir.BinarySearch(FH);
                    if(Index >= 0) throw new ApplicationException("Duplicate file name");

                    // add to the list
                    AddDir.Insert(~Index, FH);
                    }
                }

            // save compression level
            CompLevel = (Int32) CompLevelUpDown.Value;

            // close screen
            DialogResult = DialogResult.OK;
            return;
            }

        // error
        catch(Exception Ex)
            {
            // error exit
            String[] ExceptionStack = ExceptionReport.GetMessageAndStack(this, Ex);
            MessageBox.Show(this, "Add files and folders error\n" + ExceptionStack[0] + "\n" + ExceptionStack[1],
                "Add Files Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            return;
            }
        }

    ////////////////////////////////////////////////////////////////////
    //	Process selected directory
    ////////////////////////////////////////////////////////////////////

    private void ProcessDirectory
            (
            DirectoryInfo	DirInfo
            )
        {
        // process all subdirectories
        foreach(DirectoryInfo Dir in DirInfo.GetDirectories()) ProcessDirectory(Dir);

        // process all files in this directory
        foreach(FileInfo FI in DirInfo.GetFiles())
            {
            // create zip directory file header record with name date time and attributes
            FileHeader FH = new FileHeader(FI.FullName.Substring(RootDirPtr), FI.LastWriteTime, FI.Attributes, 0, FI.Length);

            // find item poition in the list
            Int32 Index = AddDir.BinarySearch(FH);
            if(Index >= 0) throw new ApplicationException("Duplicate file name");

            // add to the list
            AddDir.Insert(~Index, FH);
            }

        // add the directory name to the list
        // create zip directory file header record with name date time and attributes
        FileHeader DH = new FileHeader(DirInfo.FullName.Substring(RootDirPtr) + "\\", DirInfo.LastWriteTime, DirInfo.Attributes, 0, 0);

        // find item poition in the list
        Int32 DirIndex = AddDir.BinarySearch(DH);
        if(DirIndex >= 0) throw new ApplicationException("Duplicate directory name");

        // add to the list
        AddDir.Insert(~DirIndex, DH);

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    // Test for special system directories
    ////////////////////////////////////////////////////////////////////

    private Boolean IsSpecialDirectory
            (
            String	Name
            )
        {
        // test for two special cases
        if(String.Compare(Name, "$Recycle.Bin", true) == 0) return(true);
        if(String.Compare(Name, "System Volume Information", true) == 0) return(true);
        return(false);
        }

    ////////////////////////////////////////////////////////////////////
    // Format file size
    ////////////////////////////////////////////////////////////////////

    private static String FormatFileSize
            (
            Int64		Size
            )
        {
        return(String.Format("{0:#,##0} KB", (Size + 999) / 1000));
        }

    ////////////////////////////////////////////////////////////////////
    // Format file date and time
    ////////////////////////////////////////////////////////////////////

    private static String FormatFileDateTime
            (
            DateTime	FileDate
            )
        {
        return(String.Format("{0:yyyy}/{0:MM}/{0:dd} {0:HH}:{0:mm}:{0:ss}", FileDate));
        }

    ////////////////////////////////////////////////////////////////////
    // Format file attributes
    ////////////////////////////////////////////////////////////////////

    private static String FormatFileAttributes
            (
            FileAttributes Attr
            )
        {
        return(String.Format("{0}{1}{2}",
            (Attr & FileAttributes.System) != 0 ? "S" : "",
            (Attr & FileAttributes.Hidden) != 0 ? "H" : "",
            (Attr & FileAttributes.ReadOnly) != 0 ? "R" : ""));
        }

    ////////////////////////////////////////////////////////////////////
    //	On Resize
    ////////////////////////////////////////////////////////////////////

    private void OnResize
            (
            object sender,
            EventArgs e
            )
        {
        // protect against minimize button
        if(ClientSize.Width == 0) return;

        // button group
        ButtonsGroupBox.Left = (ClientSize.Width - ButtonsGroupBox.Width) / 2;
        ButtonsGroupBox.Top = ClientSize.Height - ButtonsGroupBox.Height;

        // split container
        FileArea.Left = 0;
        FileArea.Width = ClientSize.Width;
        FileArea.Top = 0;
        FileArea.Height = ButtonsGroupBox.Top;

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	On Resize
    ////////////////////////////////////////////////////////////////////

    private void OnFileAreaResize
            (
            object sender,
            EventArgs e
            )
        {
        // change splitter line
        FileArea.SplitterDistance = FileArea.Width / 3;

        // exit
        return;
        }

    ////////////////////////////////////////////////////////////////////
    // Resize panel 1
    ////////////////////////////////////////////////////////////////////

    private void OnFileAreaPanel1Resize
            (
            object sender,
            EventArgs e
            )
        {
        DirectoryTree.Left = 0;
        DirectoryTree.Width = FileArea.Panel1.Width - 1;
        DirectoryTree.Top = 0;
        DirectoryTree.Height = FileArea.Panel1.Height;
        return;
        }

    ////////////////////////////////////////////////////////////////////
    // Resize panel 2
    ////////////////////////////////////////////////////////////////////

    private void OnFileAreaPanel2Resize
            (
            object sender,
            EventArgs e
            )
        {
        FileList.Left = 1;
        FileList.Width = FileArea.Panel2.Width - 1;
        FileList.Top = 0;
        FileList.Height = FileArea.Panel2.Height;
        return;
        }

    ////////////////////////////////////////////////////////////////////
    // On resize file list
    ////////////////////////////////////////////////////////////////////

    private void OnFileListResize
            (
            object sender,
            EventArgs e
            )
        {
        if(ResizeLock) return;
        ResizeLock = true;

        // get graphics object
        Graphics GR = CreateGraphics();

        // file list font
        Font ListFont = FileList.Font;

        // file name
        Int32 MinWidth = (Int32) Math.Ceiling(GR.MeasureString("File Name0000", ListFont).Width);

        // Date and time width
        FileList.Columns[1].Width = (Int32) Math.Ceiling(GR.MeasureString("2000/12/12 00:00:000", ListFont).Width);

        // attribute
        FileList.Columns[2].Width = (Int32) Math.Ceiling(GR.MeasureString("Attr.0", ListFont).Width);

        // size
        FileList.Columns[3].Width = (Int32) Math.Ceiling(GR.MeasureString("0,000,000 KB", ListFont).Width);

        // list is empty
        // Note: C# has a bug.
        // When FileList.Items.Clear() is executed, the system removes all items,
        // calls the resize routine while the count is unchanged.
        if(FileList.Items.Count == 0 || FileList.Items[0] == null)
            {
            FileList.Columns[0].Width = MinWidth;
            ResizeLock = false;
            return;
            }

        // check file name
        Int32 Space = FileList.ClientRectangle.Width - FileList.Columns[1].Width - FileList.Columns[2].Width - FileList.Columns[3].Width;
        if(Space < MinWidth) Space = MinWidth;
        FileList.Columns[0].AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent);
        if(FileList.Columns[0].Width > Space) FileList.Columns[0].Width = Space;
        ResizeLock = false;
        return;
        }

    ////////////////////////////////////////////////////////////////////
    //	On Closing
    ////////////////////////////////////////////////////////////////////

    private void OnClosing
            (
            object sender,
            FormClosingEventArgs e
            )
        {
        return;
        }
    }
}